/*
 * jPOS Project [http://jpos.org]
 * Copyright (C) 2000-2015 Alejandro P. Revilla
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.jpos.q2;


import org.jdom.Element;
import org.jpos.core.Configurable;
import org.jpos.core.Configuration;
import org.jpos.core.ConfigurationException;
import org.jpos.core.XmlConfigurable;
import org.jpos.q2.qbean.QConfig;
import org.jpos.util.LogSource;
import org.jpos.util.Logger;
import org.jpos.util.NameRegistrar;

import javax.management.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.util.*;

/**
 * @author <a href="mailto:taherkordy@dpi2.dpi.net.ir">Alireza Taherkordi</a>
 * @author <a href="mailto:apr@cs.com.uy">Alejandro P. Revilla</a>
 * @version $Revision$ $Date$
 */
@SuppressWarnings("unchecked")
public class QFactory {
	ObjectName loaderName;
	Q2 q2;
	ResourceBundle classMapping;
	ConfigurationFactory defaultConfigurationFactory = new SimpleConfigurationFactory();

	public QFactory(ObjectName loaderName, Q2 q2) {
		super();
		this.loaderName = loaderName;
		this.q2 = q2;
		classMapping = ResourceBundle.getBundle(this.getClass().getName());
	}

	@SuppressWarnings("PMD.EmptyCatchBlock")
	public Object instantiate(Q2 server, Element e)
			throws ReflectionException,
			MBeanException,
			InstanceNotFoundException {
		String clazz = e.getAttributeValue("class");
		if (clazz == null) {
			try {
				clazz = classMapping.getString(e.getName());
			} catch (MissingResourceException ex) {
				// no class attribute, no mapping
				// let MBeanServer do the yelling
			}
		}
		MBeanServer mserver = server.getMBeanServer();
		getExtraPath(server.getLoader(), e);
		return mserver.instantiate(clazz, loaderName);
	}

	public ObjectInstance createQBean(Q2 server, Element e, Object obj)
			throws ClassNotFoundException,
			InstantiationException,
			IllegalAccessException,
			MalformedObjectNameException,
			MalformedURLException,
			InstanceAlreadyExistsException,
			InstanceNotFoundException,
			MBeanException,
			NotCompliantMBeanException,
			InvalidAttributeValueException,
			ReflectionException,
			ConfigurationException {
		String name = e.getAttributeValue("name");
		if (name == null)
			name = e.getName();

		ObjectName objectName = new ObjectName(Q2.QBEAN_NAME + name);
		MBeanServer mserver = server.getMBeanServer();
		if (mserver.isRegistered(objectName)) {
			throw new InstanceAlreadyExistsException(name + " has already been deployed in another file.");
		}
		ObjectInstance instance = mserver.registerMBean(
				obj, objectName
		);
		try {
			setAttribute(mserver, objectName, "Name", name);
			String logger = e.getAttributeValue("logger");
			if (logger != null)
				setAttribute(mserver, objectName, "Logger", logger);
			String realm = e.getAttributeValue("realm");
			if (realm != null)
				setAttribute(mserver, objectName, "Realm", realm);
			setAttribute(mserver, objectName, "Server", server);
			setAttribute(mserver, objectName, "Persist", e);
			configureQBean(mserver, objectName, e);
			setConfiguration(obj, e);  // handle legacy (QSP v1) Configurables

			if (obj instanceof QBean)
				mserver.invoke(objectName, "init", null, null);
		} catch (ConfigurationException ce) {
			mserver.unregisterMBean(objectName);
			ce.fillInStackTrace();
			throw ce;
		}

		return instance;
	}

	public Q2 getQ2() {
		return q2;
	}

	public void getExtraPath(QClassLoader loader, Element e) {
		Element classpathElement = e.getChild("classpath");
		if (classpathElement != null) {
			try {
				loader = loader.scan(true);    // force a new classloader
			} catch (Throwable t) {
				getQ2().getLog().error(t);
			}
			for (Object o : classpathElement.getChildren("url")) {
				Element u = (Element) o;
				try {
					loader.addURL(u.getTextTrim());
				} catch (MalformedURLException ex) {
					q2.getLog().warn(u.getTextTrim(), ex);
				}
			}
		}
	}

	@SuppressWarnings("PMD.EmptyCatchBlock")
	public void setAttribute
			(MBeanServer server, ObjectName objectName,
			 String attribute, Object value)
			throws InstanceNotFoundException,
			MBeanException,
			InvalidAttributeValueException,
			ReflectionException {
		try {
			server.setAttribute(
					objectName, new Attribute(attribute, value)
			);
		} catch (AttributeNotFoundException ex) {
			// okay to fail
		} catch (InvalidAttributeValueException ex) {
			// okay to fail (produced by some application servers instead of AttributeNotFoundException)
		}
	}

	public void startQBean(Q2 server, ObjectName objectName)
			throws ClassNotFoundException,
			InstantiationException,
			IllegalAccessException,
			MalformedObjectNameException,
			MalformedURLException,
			InstanceAlreadyExistsException,
			InstanceNotFoundException,
			MBeanException,
			NotCompliantMBeanException,
			InvalidAttributeValueException,
			ReflectionException {
		MBeanServer mserver = server.getMBeanServer();
		mserver.invoke(objectName, "start", null, null);
	}

	public void destroyQBean(Q2 server, ObjectName objectName, Object obj)
			throws ClassNotFoundException,
			InstantiationException,
			IllegalAccessException,
			MalformedObjectNameException,
			MalformedURLException,
			InstanceAlreadyExistsException,
			InstanceNotFoundException,
			MBeanException,
			NotCompliantMBeanException,
			InvalidAttributeValueException,
			ReflectionException {
		MBeanServer mserver = server.getMBeanServer();
		if (obj instanceof QBean) {
			mserver.invoke(objectName, "stop", null, null);
			mserver.invoke(objectName, "destroy", null, null);
		}
		if (objectName != null)
			mserver.unregisterMBean(objectName);
	}

	public void configureQBean(MBeanServer server, ObjectName objectName, Element e)
			throws ConfigurationException {
		try {
			AttributeList attributeList = getAttributeList(e);
			for (Object anAttributeList : attributeList)
				server.setAttribute(objectName, (Attribute) anAttributeList);
		} catch (Exception e1) {
			throw new ConfigurationException(e1);
		}

	}

	public AttributeList getAttributeList(Element e)
			throws ConfigurationException {
		AttributeList attributeList = new AttributeList();
		List childs = e.getChildren("attr");
		for (Object child : childs) {
			Element childElement = (Element) child;
			String name = childElement.getAttributeValue("name");
			name = getAttributeName(name);
			Attribute attr = new Attribute(name, getObject(childElement));
			attributeList.add(attr);
		}
		return attributeList;
	}

	/**
	 * Creates an object from a definition element.
	 * The element may have an attribute called type indicating the type of the object
	 * to create, if this attribute is not present java.lang.String is assumed.
	 * int, long and boolean are converted to their wrappers.
	 *
	 * @param childElement Dom Element with the definition of the object.
	 * @return The created object.
	 * @throws ConfigurationException If an exception is found trying to create the object.
	 */
	@SuppressWarnings("unchecked")
	protected Object getObject(Element childElement)
			throws ConfigurationException {
		String type = childElement.getAttributeValue("type", "java.lang.String");
		if ("int".equals(type))
			type = "java.lang.Integer";
		else if ("long".equals(type))
			type = "java.lang.Long";
		else if ("boolean".equals(type))
			type = "java.lang.Boolean";

		String value = childElement.getText();
		try {
			Class attributeType = Class.forName(type);
			if (Collection.class.isAssignableFrom(attributeType))
				return getCollection(attributeType, childElement);
			else {
				Class[] parameterTypes = {"".getClass()};
				Object[] parameterValues = {value};
				return attributeType.getConstructor(parameterTypes).newInstance(parameterValues);
			}
		} catch (Exception e1) {
			throw new ConfigurationException(e1);
		}

	}


	/**
	 * Creats a collection from a definition element with the format.
	 * <PRE>
	 * <{attr|item} type="...">
	 * <item [type="..."]>...</item>
	 * ...
	 * </attr>
	 * </PRE>
	 *
	 * @param type class type
	 * @param e    the Element
	 * @return the object collection
	 * @throws ConfigurationException
	 */
	@SuppressWarnings("unchecked")
	protected Collection getCollection(Class type, Element e)
			throws ConfigurationException {
		try {
			Collection<Object> col = (Collection<Object>) type.newInstance();
			for (Object o : e.getChildren("item")) {
				col.add(getObject((Element) o));
			}
			return col;
		} catch (Exception e1) {
			throw new ConfigurationException(e1);
		}
	}

	/**
	 * sets the first character of the string to the upper case
	 *
	 * @param name attribute name
	 * @return attribute name
	 */
	public String getAttributeName(String name) {
		StringBuilder tmp = new StringBuilder(name);
		if (tmp.length() > 0)
			tmp.setCharAt(0, name.toUpperCase().charAt(0));
		return tmp.toString();
	}

	public Object newInstance(String clazz)
			throws ConfigurationException {
		try {
			MBeanServer mserver = q2.getMBeanServer();
			return mserver.instantiate(clazz, loaderName);
		} catch (Exception e) {
			throw new ConfigurationException(clazz, e);
		}
	}

	public Configuration getConfiguration(Element e)
			throws ConfigurationException {
		String configurationFactoryClazz = e.getAttributeValue("configuration-factory");
		ConfigurationFactory cf = configurationFactoryClazz != null ?
				(ConfigurationFactory) newInstance(configurationFactoryClazz) : defaultConfigurationFactory;

		Configuration cfg = cf.getConfiguration(e);
		String merge = e.getAttributeValue("merge-configuration");
		if (merge != null) {
			StringTokenizer st = new StringTokenizer(merge, ", ");
			while (st.hasMoreElements()) {
				try {
					Configuration c = QConfig.getConfiguration(st.nextToken());
					for (String k : c.keySet()) {
						if (cfg.get(k, null) == null) {
							String[] v = c.getAll(k);
							switch (v.length) {
								case 0:
									break;
								case 1:
									cfg.put(k, v[0]);
									break;
								default:
									cfg.put(k, v);
							}
						}
					}
				} catch (NameRegistrar.NotFoundException ex) {
					throw new ConfigurationException(ex.getMessage());
				}
			}
		}
		return cfg;
	}

	public void setLogger(Object obj, Element e) {
		if (obj instanceof LogSource) {
			String loggerName = e.getAttributeValue("logger");
			if (loggerName != null) {
				String realm = e.getAttributeValue("realm");
				if (realm == null)
					realm = e.getName();
				Logger logger = Logger.getLogger(loggerName);
				((LogSource) obj).setLogger(logger, realm);
			}
		}
	}

	public void setConfiguration(Object obj, Element e)
			throws ConfigurationException {
		try {
			if (obj instanceof Configurable)
				((Configurable) obj).setConfiguration(getConfiguration(e));
			if (obj instanceof XmlConfigurable)
				((XmlConfigurable) obj).setConfiguration(e);
		} catch (ConfigurationException ex) {
			throw new ConfigurationException(ex);
		}
	}

	/**
	 * Try to invoke a method (usually a setter) on the given object
	 * silently ignoring if method does not exist
	 *
	 * @param obj the object
	 * @param m   method to invoke
	 * @param p   parameter
	 * @throws ConfigurationException if method happens to throw an exception
	 */
	public static void invoke(Object obj, String m, Object p)
			throws ConfigurationException {
		invoke(obj, m, p, p != null ? p.getClass() : null);
	}

	/**
	 * Try to invoke a method (usually a setter) on the given object
	 * silently ignoring if method does not exist
	 *
	 * @param obj the object
	 * @param m   method to invoke
	 * @param p   parameter
	 * @param pc  parameter class
	 * @throws ConfigurationException if method happens to throw an exception
	 */
	@SuppressWarnings("PMD.EmptyCatchBlock")
	public static void invoke(Object obj, String m, Object p, Class pc)
			throws ConfigurationException {
		try {
			if (p != null) {
				Class[] paramTemplate = {pc};
				Method method = obj.getClass().getMethod(m, paramTemplate);
				Object[] param = new Object[1];
				param[0] = p;
				method.invoke(obj, param);
			} else {
				Method method = obj.getClass().getMethod(m);
				method.invoke(obj);
			}
		} catch (NoSuchMethodException ignored) {
		} catch (NullPointerException ignored) {
		} catch (IllegalAccessException ignored) {
		} catch (InvocationTargetException e) {
			throw new ConfigurationException(
					obj.getClass().getName() + "." + m + "(" + p + ")",
					e.getTargetException()
			);
		}
	}
}
